/*
 * umlapi 1.1.2
 * UML API 
 * Copyright (C) 2003  Pavel Vlasov
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2 of the License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 *
 * URL: http://www.pavelvlasov.com/Products/UmlApi/index.html
 * e-Mail: vlasov@pavelvlasov.com

 */
package com.pavelvlasov.uml.xmi;

import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.Set;

import javax.xml.transform.TransformerException;

import org.apache.oro.text.GlobCompiler;
import org.apache.oro.text.regex.MalformedPatternException;
import org.apache.oro.text.regex.Pattern;
import org.apache.oro.text.regex.Perl5Matcher;
import org.w3c.dom.Element;

import com.pavelvlasov.uml.Advice;
import com.pavelvlasov.uml.Aspect;
import com.pavelvlasov.uml.AspectInstance;
import com.pavelvlasov.uml.Attribute;
import com.pavelvlasov.uml.Classifier;
import com.pavelvlasov.uml.Dependency;
import com.pavelvlasov.uml.Model;
import com.pavelvlasov.uml.ModelElement;
import com.pavelvlasov.uml.Operation;
import com.pavelvlasov.uml.Parameter;
import com.pavelvlasov.uml.Visitor;

/**
 * Resolves aspects based on stereotypes, 
 * matches aspects and advices based on regular (glob) 
 * expressions placed in square brackets after element/operation name.
 * Examples: XmlPersistenceAspect[*XmlBean], audit[set*].
 * An element is considered to be a global aspect if it has <<aspect>> stereotype.
 * An element is considered to be an aspect for a another element if there is a
 * dependency relationship with <<aspect>> stereotype between the two.
 * Operation is considered to be an advice if it has <<pre>> <<post>> or <<around>>
 * stereotype.
 * @author Pavel Vlasov	
 * @version $Revision$
 */
public class StereotypeGlobAspectResolver implements AspectResolver {

	public static final String FACET = "facet";
	public static final String ASPECT_STEREOTYPE = "aspect";
	public static final String ELLIPSIS = "...";
	
	public static final String PUBLIC = "public";
	public static final String PROTECTED = "protected";
	public static final String PRIVATE = "private";
	public static final String PACKAGE = "package";
	
	private ModelImpl model;
	private final Collection aspects=new LinkedList();
	private boolean loaded=false;
	
	private GlobCompiler compiler=new GlobCompiler();
	private Perl5Matcher matcher=new Perl5Matcher();
	
	private class AspectEntry {
		AspectImpl aspectImpl;
		Pattern pattern;
		
		AspectEntry(AspectImpl aspectImpl) {
			try {
				this.aspectImpl=aspectImpl;
				String aspectName=aspectImpl.getName();
				int bi=aspectName.indexOf('[');
				if (bi!=-1 && aspectName.charAt(aspectName.length()-1)==']') {
					pattern=compiler.compile(aspectName.substring(bi+1,aspectName.length()-1));
					aspectImpl.setName(aspectName.substring(0,bi));	
				}
			} catch (MalformedPatternException e) {
				pattern=null;
				model.getLogger().error(aspectImpl, "Malformed pattern: "+e);
			}				
		}
		
		/**
		 * Matches if 
		 * a) Aspect has public visibility or 
		 * b) Aspect has package visibility and is in the same package with classifier or
		 * c) Aspect has protected scope and classifier is in the same package or below
		 * d) Aspect has private scope and classifier is a "subelement" of the aspect
		 * 
		 * If aspect name contains . then classifier absolute name is used for matching. 
		 * @param classifier
		 * @return
		 */
		boolean matches(Classifier classifier) {
			if (PUBLIC.equals(aspectImpl.getVisibility())) {
				return nameMatches(classifier);
			} else if (PACKAGE.equals(aspectImpl.getVisibility())) {
				return aspectImpl.getOwner().getId().equals(classifier.getOwner().getId()) && nameMatches(classifier);
			} else if (PROTECTED.equals(aspectImpl.getVisibility())) {
				return classifier.getAbsoluteName().startsWith(aspectImpl.getOwner().getAbsoluteName()) && nameMatches(classifier);
			} else if (PRIVATE.equals(aspectImpl.getVisibility())) {
				return classifier.getAbsoluteName().startsWith(aspectImpl.getAbsoluteName()) && nameMatches(classifier);
			} else {
				model.getLogger().error(aspectImpl, "Invalid aspect visibility: "+aspectImpl.getVisibility());
				return false; // Don't throw an exception because should be lenient.
			}
		}
		
		private boolean nameMatches(Classifier classifier) {
			return pattern!=null && matcher.matches(aspectImpl.getName().indexOf('.')==-1 ? classifier.getName() : classifier.getAbsoluteName(), pattern);
		}
		
		AspectImpl getAspectImpl() {
			return aspectImpl;
		}
	}

	/**
	 * 
	 */
	public StereotypeGlobAspectResolver() {
		super();
	}
	
	private Collection getAspects() {
		if (!loaded) {
			model.accept(new Visitor() {
	
				public void visit(Model model) {
					
				}
	
				public void visit(ModelElement modelElement) {
					if (modelElement instanceof AspectImpl) {
						aspects.add(new AspectEntry((AspectImpl) modelElement));
					}				
				}});
				
			loaded=true;
			model.getLogger().debug(null, "Loaded "+aspects.size()+" global aspects");
		}
		return aspects;
	}

//	private Collection instanceCache=new LinkedList(); 
	
	/* (non-Javadoc)
	 * @see com.pavelvlasov.uml.xmi.AspectResolver#getAspectInstances(com.pavelvlasov.uml.Classifier)
	 */
	public Collection getAspectInstances(Classifier classifier) {
		model.getLogger().debug(classifier, "Resolving aspect instances");
		Collection ret=new LinkedList();
		Iterator it=getAspects().iterator();
		while (it.hasNext()) {
			AspectEntry entry=(AspectEntry) it.next();
			if (entry.matches(classifier)) {
//				boolean found=false;
//				Iterator cit=instanceCache.iterator();
//				while (cit.hasNext()) {
//					AspectInstance ai=(AspectInstance) cit.next();
//					if (ai.getAspected().equals(classifier) && ai.getId().equals(entry.getAspect().getId())) {
//						ret.add(ai);
//						found=true;
//						break;
//					}
//				}
//				
//				if (!found) {
					AspectImpl aspectImpl=entry.getAspectImpl();
					AspectInstance ai=new AspectInstanceImpl(aspectImpl, classifier, getAdvices(aspectImpl, classifier));
//					instanceCache.add(ai);
					ret.add(ai);
//				}				
			}
		}
		
		Iterator cdit=classifier.getClientDependencies().iterator();
		while (cdit.hasNext()) {
			Dependency cd=(Dependency) cdit.next();
			if (ASPECT_STEREOTYPE.equals(cd.getStereotype())) {
				ClassifierImpl aci=(ClassifierImpl) cd.getSupplier();
				AspectImpl aspectImpl=new AspectImpl(aci);
				ret.add(new AspectInstanceImpl(aspectImpl, classifier, getAdvices(aspectImpl, classifier)));
			}
		}
				
		return ret;
	}

	/**
	 * @param aspect
	 * @param classifier
	 * @return
	 */
	private Collection getAdvices(Aspect aspect, Classifier classifier) {
		Collection ret=new LinkedList();
		Iterator aopit=aspect.getOperations().iterator();
		while (aopit.hasNext()) {
			Operation aop=(Operation) aopit.next();
			Iterator copit=classifier.getOperations().iterator();
			while (copit.hasNext()) {
				Operation cop=(Operation) copit.next();
				if (matches(aop, cop)) {
					ret.add(new AdviceImpl(aop, cop));
				}
			}
		}
		return ret;
	}

	public static final Set ADVICE_NAMES;
	
	static {
		Set an=new HashSet();
		an.add("pre");
		an.add("around");
		an.add("post");
		an.add(FACET);
		ADVICE_NAMES=Collections.unmodifiableSet(an);
	}

	/**
	 * Matches if a) Name matches and b) Parameters match. 
	 * Parameters match if a) Parameter type name matches. If advice parameter name contains dot(s)
	 * then absolute name of classifier parameter is used for matching or b) Classifier parameter type is
	 * a subtype of advice parameter type.
	 * Parameter with type ... designates any number of parameters (even 0).  
	 * @param aop
	 * @param cop
	 * @return
	 */
	private boolean matches(Operation aop, Operation cop) {
		String copName=cop.getName();
		Collection parameterClassifiers=new LinkedList();
		Iterator coppit=cop.getParameters().iterator();
		while (coppit.hasNext()) {
			Parameter p=(Parameter) coppit.next();
			if (!"return".equals(p.getKind())) {
				parameterClassifiers.add(p.getClassifier());
			}
		}
		return matches(aop, copName, parameterClassifiers);
	}		
	
	private boolean matches(Operation aop, String copName, Collection parameterClassifiers) {		
		if (ADVICE_NAMES.contains(aop.getStereotype())) {
			// Match method name
			try {
				Pattern pattern = compiler.compile(aop.getName());
				if (matcher.matches(copName, pattern)) {
					Collection apc=new LinkedList();
					
					Iterator apit=aop.getParameters().iterator();					
					while (apit.hasNext()) {
						Parameter p=(Parameter) apit.next();
						if (!"return".equals(p.getKind())) {
							apc.add(p.getClassifier());
						}
					}
					
					if (apc.size()==0 && parameterClassifiers.size()==0) {
						return true;
					}	
					
					Iterator apcit=apc.iterator();
					Iterator pcit=parameterClassifiers.iterator();
					while (apcit.hasNext()) {
						Classifier ac=(Classifier) apcit.next();
						
						String apcName = ac.getName();
						if (ELLIPSIS.equals(apcName)) {
							return true;
						} else {
							if (pcit.hasNext()) {
								Classifier cpc=(Classifier) pcit.next();
								Pattern pp=compiler.compile(apcName);
								
								// Match parameter type name
								if (matcher
									.matches(
										apcName.indexOf('.') == -1
											? cpc.getName()
											: cpc.getAbsoluteName(),
										pp)) {
									// Parameter matched
									continue;
								} else if (cpc.isTypeOf(ac)) {
									// Parameter is a subtype of aspect
									// parameter
									continue;
								} else {
									// Didn't match
									return false;
								}
							} else {
								return false;
							}
						}
					}
					return !pcit.hasNext();
				} else {
					return false;
				}			
			} catch (MalformedPatternException e) {
				model.getLogger().error(aop, "Malformed pattern: "+e);
				return false;
			}			
		} else {
			return false;
		}
	}

	/* (non-Javadoc)
	 * @see com.pavelvlasov.uml.xmi.AspectResolver#getAdvices(com.pavelvlasov.uml.Operation)
	 */
	public Collection getAdvices(Operation operation) {
		Collection ret=new LinkedList();
		Classifier c=(Classifier) operation.getOwner();
		Iterator aiit=c.getAspectInstances().iterator();
		while (aiit.hasNext()) {
			AspectInstance ai=(AspectInstance) aiit.next();
			Iterator ait=ai.getAdvices().iterator();
			while (ait.hasNext()) {
				Advice a=(Advice) ait.next();
				if (a.getAdvised().getId().equals(operation.getId())) {
					ret.add(a);
				}
			}
		}
		return ret;
	}

	/** 
	 * @return true if element has <<aspect>> stereotype.
	 */
	public boolean isAspect(Element holder) {		
		try {
			return ASPECT_STEREOTYPE.equals(ModelElementImpl.getElementStereotype(holder, model.getCachedXPathAPI()));
		} catch (TransformerException e) {
			model.getLogger().error(null, "Can't read stereotype");
			return false;
		}
	}

	/* (non-Javadoc)
	 * @see com.pavelvlasov.uml.xmi.AspectResolver#setModel(com.pavelvlasov.uml.xmi.ModelImpl)
	 */
	public void setModel(ModelImpl model) {
		this.model=model;
	}

	/* (non-Javadoc)
	 * @see com.pavelvlasov.uml.xmi.AspectResolver#getAspectInstances(com.pavelvlasov.uml.Aspect)
	 */
	public Collection getAspectInstances(final AspectImpl aspectImpl) {
		final Collection ret=new LinkedList();
		model.accept(new Visitor() {
			private AspectEntry entry=new AspectEntry(aspectImpl);
	
			public void visit(Model model) {
					
			}
	
			public void visit(ModelElement modelElement) {
				if (modelElement instanceof Classifier
					&& entry.matches((Classifier) modelElement)) {
					ret.add(
						new AspectInstanceImpl(
							aspectImpl,
							(Classifier) modelElement,
							getAdvices(aspectImpl, (Classifier) modelElement)));
				}
			}});
			
		return ret;
	}

	public Collection getAdvices(Aspect aspect, String operationName, Collection parameterClassifiers) {
		Collection ret=new LinkedList();
		Iterator aopit=aspect.getOperations().iterator();
		while (aopit.hasNext()) {
			Operation aop=(Operation) aopit.next();
			if (matches(aop, operationName, parameterClassifiers)) {
				ret.add(new AdviceImpl(aop, null));
			}
		}
		return ret;
	}

	public Collection getFacets(Attribute attribute) {
		Collection ret = new LinkedList();
		Classifier c = (Classifier) attribute.getOwner();
		Iterator aiit = c.getAspectInstances().iterator();
		while (aiit.hasNext()) {
			AspectInstance ai = (AspectInstance) aiit.next();
			Iterator ait = ai.getAttributes().iterator();
			while (ait.hasNext()) {
				Attribute fat = (Attribute) ait.next();
				if (FACET.equals(fat.getStereotype())) {
					// Match attribute name
					try {
						Pattern pattern = compiler.compile(fat.getName());
						if (matcher.matches(attribute.getName(), pattern)) {
							Pattern tp =
								compiler.compile(fat.getClassifier().getName());

							// Match type name
							if (matcher
								.matches(
									fat.getClassifier().getName().indexOf('.')
										== -1
										? attribute.getClassifier().getName()
										: attribute
											.getClassifier()
											.getAbsoluteName(),
									tp)) {
								// Parameter matched
								ret.add(fat);
							} else if (
								attribute.getClassifier().isTypeOf(
									fat.getClassifier())) {
								ret.add(fat);
							}
						}
					} catch (MalformedPatternException e) {
						model.getLogger().error(fat,"Malformed pattern: " + e);
					}
				}
			}
		}
		return ret;
	}

	/* (non-Javadoc)
	 * @see com.pavelvlasov.uml.xmi.AspectResolver#isAdvice(com.pavelvlasov.uml.Operation)
	 */
	public boolean isAdvice(Operation operation) {
		return ADVICE_NAMES.contains(operation.getStereotype());
	}

	/* (non-Javadoc)
	 * @see com.pavelvlasov.uml.xmi.AspectResolver#isFacet(com.pavelvlasov.uml.Attribute)
	 */
	public boolean isFacet(Attribute attribute) {
		return FACET.equals(attribute.getStereotype());
	}
}
